中文

了解事件溯源如何革新您的审计追踪实现,提供无与伦比的可追溯性、数据完整性和系统弹性。探讨实际示例和实现策略。

事件溯源:为健壮且可追溯的系统实现审计追踪

在当今复杂且互联的数字环境中,维护一个健壮且全面的审计追踪至关重要。它不仅常常是监管要求,而且对于调试、安全分析和理解系统演进也至关重要。事件溯源是一种架构模式,它将应用程序状态的所有更改捕获为一系列事件,为实现可靠、可审计且可扩展的审计追踪提供了一种优雅而强大的解决方案。

什么是事件溯源?

传统的应用程序通常只在数据库中存储数据的当前状态。这种方法使得重建过去的状态或理解导致当前状态的一系列事件变得困难。相比之下,事件溯源专注于将应用程序状态的每一次重大更改捕获为不可变的事件。这些事件存储在仅附加的事件存储中,形成系统内所有操作的完整按时间顺序记录。

将其想象成银行账户账本。它不只记录当前余额,而是将每一次存款、取款和转账都记录为单独的事件。通过重放这些事件,您可以在任何时间点重建账户的状态。

为什么为审计追踪使用事件溯源?

事件溯源在实现审计追踪方面提供了几个引人注目的优势:

实现事件溯源进行审计追踪:分步指南

以下是为审计追踪实现事件溯源的实用指南:

1. 识别关键事件

第一步是识别您要在审计追踪中捕获的关键事件。这些事件应代表应用程序状态的重大更改。考虑诸如以下操作:

示例:对于电子商务平台,关键事件可能包括“订单创建”、“收到付款”、“订单发货”、“添加到购物车的产品”和“用户个人资料更新”。

2. 定义事件结构

每个事件都应具有包含以下信息的结构良好的定义:

示例:“OrderCreated”事件可能具有以下结构:

{
  "eventType": "OrderCreated",
  "eventData": {
    "orderId": "12345",
    "customerId": "67890",
    "orderDate": "2023-10-27T10:00:00Z",
    "totalAmount": 100.00,
    "currency": "USD",
    "shippingAddress": {
      "street": "123 Main St",
      "city": "Anytown",
      "state": "CA",
      "zipCode": "91234",
      "country": "USA"
    }
  },
  "timestamp": "2023-10-27T10:00:00Z",
  "userId": "user123",
  "transactionId": "tx12345",
  "correlationId": "corr123",
  "metadata": {
    "ipAddress": "192.168.1.1",
    "browser": "Chrome",
    "location": {
       "latitude": 34.0522,
       "longitude": -118.2437
    }
  }
}

3. 选择事件存储

事件存储是用于存储事件的中央存储库。它应该是一个针对写入和读取事件序列进行优化的仅附加数据库。有几种选项可用:

选择事件存储时,请考虑以下因素:

4. 实现事件发布

当事件发生时,您的应用程序需要将其发布到事件存储。这通常涉及以下步骤:

示例(使用假设的 EventStoreService):

public class OrderService {

  private final EventStoreService eventStoreService;

  public OrderService(EventStoreService eventStoreService) {
    this.eventStoreService = eventStoreService;
  }

  public void createOrder(Order order, String userId) {
    // ... 业务逻辑创建订单 ...

    OrderCreatedEvent event = new OrderCreatedEvent(
        order.getOrderId(),
        order.getCustomerId(),
        order.getOrderDate(),
        order.getTotalAmount(),
        order.getCurrency(),
        order.getShippingAddress()
    );

    eventStoreService.appendEvent("order", order.getOrderId(), event, userId);
  }
}

public class EventStoreService {

  public void appendEvent(String streamName, String entityId, Object event, String userId) {
    // 创建事件对象
    EventRecord eventRecord = new EventRecord(
        UUID.randomUUID(), // eventId
        streamName,  // streamName
        entityId,   // entityId
        event.getClass().getName(), // eventType
        toJson(event),  // eventData
        Instant.now().toString(), // timestamp
        userId  // userId
    );

    // 序列化事件
    String serializedEvent = toJson(eventRecord);

    // 将事件附加到事件存储(实现特定于所选事件存储)
    storeEventInDatabase(serializedEvent);

    // 将事件发布到订阅者(可选)
    publishEventToMessageQueue(serializedEvent);
  }

  // 数据库和消息队列交互的占位符方法
  private void storeEventInDatabase(String serializedEvent) {
    // 存储事件到数据库的实现
    System.out.println("Storing event in database: " + serializedEvent);
  }

  private void publishEventToMessageQueue(String serializedEvent) {
    // 发布事件到消息队列的实现
    System.out.println("Publishing event to message queue: " + serializedEvent);
  }

  private String toJson(Object obj) {
    // 将事件序列化为 JSON 的实现
    try {
      ObjectMapper mapper = new ObjectMapper();
      return mapper.writeValueAsString(obj);
    } catch (Exception e) {
      throw new RuntimeException("Error serializing event to JSON", e);
    }
  }
}


class EventRecord {
  private final UUID eventId;
  private final String streamName;
  private final String entityId;
  private final String eventType;
  private final String eventData;
  private final String timestamp;
  private final String userId;

  public EventRecord(UUID eventId, String streamName, String entityId, String eventType, String eventData, String timestamp, String userId) {
    this.eventId = eventId;
    this.streamName = streamName;
    this.entityId = entityId;
    this.eventType = eventType;
    this.eventData = eventData;
    this.timestamp = timestamp;
    this.userId = userId;
  }

  // Getters

  @Override
  public String toString() {
    return "EventRecord{"
        + "eventId=" + eventId
        + ", streamName='" + streamName + '\''
        + ", entityId='" + entityId + '\''
        + ", eventType='" + eventType + '\''
        + ", eventData='" + eventData + '\''
        + ", timestamp='" + timestamp + '\''
        + ", userId='" + userId + '\''
        + '}'
  }
}

class OrderCreatedEvent {
    private final String orderId;
    private final String customerId;
    private final String orderDate;
    private final double totalAmount;
    private final String currency;
    private final String shippingAddress;

    public OrderCreatedEvent(String orderId, String customerId, String orderDate, double totalAmount, String currency, String shippingAddress) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.orderDate = orderDate;
        this.totalAmount = totalAmount;
        this.currency = currency;
        this.shippingAddress = shippingAddress;
    }

    // Getters for all fields

    public String getOrderId() { return orderId; }
    public String getCustomerId() { return customerId; }
    public String getOrderDate() { return orderDate; }
    public double getTotalAmount() { return totalAmount; }
    public String getCurrency() { return currency; }
    public String getShippingAddress() { return shippingAddress; }

    @Override
    public String toString() {
        return "OrderCreatedEvent{"
                + "orderId='" + orderId + '\''
                + ", customerId='" + customerId + '\''
                + ", orderDate='" + orderDate + '\''
                + ", totalAmount=" + totalAmount
                + ", currency='" + currency + '\''
                + ", shippingAddress='" + shippingAddress + '\''
                + '}'
    }
}

class Order {
  private final String orderId;
  private final String customerId;
  private final String orderDate;
  private final double totalAmount;
  private final String currency;
  private final String shippingAddress;

  public Order(String orderId, String customerId, String orderDate, double totalAmount, String currency, String shippingAddress) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.orderDate = orderDate;
        this.totalAmount = totalAmount;
        this.currency = currency;
        this.shippingAddress = shippingAddress;
    }

    // Getters for all fields

    public String getOrderId() { return orderId; }
    public String getCustomerId() { return customerId; }
    public String getOrderDate() { return orderDate; }
    public double getTotalAmount() { return totalAmount; }
    public String getCurrency() { return currency; }
    public String getShippingAddress() { return shippingAddress; }

    @Override
    public String toString() {
        return "Order{"
                + "orderId='" + orderId + '\''
                + ", customerId='" + customerId + '\''
                + ", orderDate='" + orderDate + '\''
                + ", totalAmount=" + totalAmount
                + ", currency='" + currency + '\''
                + ", shippingAddress='" + shippingAddress + '\''
                + '}'
    }
}

5. 构建读模型(投影)

虽然事件存储提供了所有更改的完整历史记录,但直接查询它进行读取操作通常效率不高。相反,您可以构建读模型,也称为投影,它们针对特定的查询模式进行了优化。这些读模型从事件流派生,并在发布新事件时异步更新。

示例:您可以创建一个读模型,其中包含特定客户的所有订单列表,或者一个总结特定产品销售数据的读模型。

要构建读模型,请订阅事件流并处理每个事件。对于每个事件,相应地更新读模型。

示例:

public class OrderSummaryReadModelUpdater {

    private final OrderSummaryRepository orderSummaryRepository;

    public OrderSummaryReadModelUpdater(OrderSummaryRepository orderSummaryRepository) {
        this.orderSummaryRepository = orderSummaryRepository;
    }

    public void handle(OrderCreatedEvent event) {
        OrderSummary orderSummary = new OrderSummary(
                event.getOrderId(),
                event.getCustomerId(),
                event.getOrderDate(),
                event.getTotalAmount(),
                event.getCurrency()
        );

        orderSummaryRepository.save(orderSummary);
    }

    // 其他事件处理器,用于 PaymentReceivedEvent、OrderShippedEvent 等。
}

interface OrderSummaryRepository {
    void save(OrderSummary orderSummary);
}

class OrderSummary {
    private final String orderId;
    private final String customerId;
    private final String orderDate;
    private final double totalAmount;
    private final String currency;

    public OrderSummary(String orderId, String customerId, String orderDate, double totalAmount, String currency) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.orderDate = orderDate;
        this.totalAmount = totalAmount;
        this.currency = currency;
    }
    //Getters
}

6. 保护事件存储

事件存储包含敏感数据,因此妥善保护它至关重要。请考虑以下安全措施:

7. 实现审计和报告

实施事件溯源后,您可以使用事件流生成审计报告和执行安全分析。您可以查询事件存储以查找与特定用户、事务或实体相关的所有事件。您还可以使用事件流在任何时间点重建系统状态。

示例:您可能生成一份报告,显示特定用户个人资料在一段时间内的所有更改,或者一份显示特定用户发起的所有事务的报告。

考虑以下报告功能:

事件溯源的挑战

虽然事件溯源带来了许多好处,但它也带来了一些挑战:

事件溯源的最佳实践

为减轻事件溯源的挑战,请遵循以下最佳实践:

事件溯源的实际示例

事件溯源在各种行业和应用程序中使用,包括:

结论

事件溯源是一种强大的架构模式,可以彻底改变您的审计追踪实现。它提供了无与伦比的可追溯性、数据完整性和系统弹性。虽然它确实带来了一些挑战,但事件溯源的优势往往大于其成本,特别是对于复杂且关键的系统。通过遵循本指南中概述的最佳实践,您可以成功地实现事件溯源并构建健壮且可审计的系统。

进一步阅读

事件溯源:为健壮且可追溯的系统实现审计追踪 | MLOG